// C
#include <assert.h>
#include <errno.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>

// Linux
#include <dirent.h>
#include <grp.h>
#include <pwd.h>
#include <sys/stat.h>

#include "src/lta.h"
#include "src/ltbase.h"
#include "src/ltdstr.h"
#include "src/lterr.h"
#include "src/ltlog.h"
#include "src/ltopt.h"

typedef struct {
    lta_Strings files;
    bool show_dot;
    bool help;
    bool long_info;
} Option;

typedef enum {
    kFileType_File,
    kFileType_Dir,
    kFileType_Link,

    kFileType_Fifo,
    kFileType_Socket,

    kFileType_Block,
    kFileType_Char,

    kFileType_Unknown,
} FileType;

static char const* kFileTypeName[] = {
    "File", "Dir", "Link", "Fifo", "Socket", "Block", "Char", "Unknown",
};

static int ListDir_DirPtr(char const* filename, DIR* dir, Option* opt);
static int ListDir_Path(char const* filename, Option* opt);
static int ListFile_Long_DirEnt(char const* filename, struct dirent* dirent);
static void ListFile_Long(char const* name, FileType ft, struct stat* stat);

static FileType
DirEntType_To_FileType(unsigned char c) {
    switch (c) {
        case DT_BLK: return kFileType_Block;
        case DT_CHR: return kFileType_Char;
        case DT_REG: return kFileType_File;
        case DT_DIR: return kFileType_Dir;
        case DT_FIFO: return kFileType_Fifo;
        case DT_LNK: return kFileType_Link;
        case DT_SOCK: return kFileType_Socket;
        default: return kFileType_Unknown;
    }
}

static FileType
StatToFileType(int mode) {
    switch (mode) {
        case S_IFBLK: return kFileType_Block;
        case S_IFCHR: return kFileType_Char;
        case S_IFDIR: return kFileType_Dir;
        case S_IFIFO: return kFileType_Fifo;
        case S_IFLNK: return kFileType_Link;
        case S_IFREG: return kFileType_File;
        case S_IFSOCK: return kFileType_Socket;
        default: return kFileType_Unknown;
    }
}

static int
ListPath(char const* path, Option* opt) {
    struct stat stat;

    if (lstat(path, &stat) != 0) {
        ltlog_Log2(Error, "failed to get file status",
                   "file path:", String(path),
                   "error:", String(strerror(errno)));
        return 1;
    }

    FileType ft = StatToFileType(stat.st_mode & S_IFMT);

    if (ft == kFileType_Dir)
        return ListDir_Path(path, opt);

    if (opt->long_info) {
        ListFile_Long(path, ft, &stat);
    } else {
        printf("%s\n", path);
    }

    return 0;
}

static int
ListDir_Path(char const* filename, Option* opt) {
    DIR* dirptr = opendir(filename);

    if (dirptr == NULL) {
        ltlog_Log2(Error, "failed to open file", "file name", String(filename),
                   "error", String(strerror(errno)));
        return 1;
    }

    ListDir_DirPtr(filename, dirptr, opt);
    closedir(dirptr);

    return 0;
}

static int
ListDir_DirPtr(char const* filename, DIR* dirptr, Option* opt) {
    struct dirent* dirent = NULL;

    for (;;) {
        errno = 0;
        if ((dirent = readdir(dirptr)) == NULL) {
            if (errno != 0) {
                ltlog_Log1(Error, "failed to read dir",
                           "error:", String(strerror(errno)));
                return 1;
            }
            break;
        }

        if (!opt->show_dot && dirent->d_name[0] == '.')
            continue;

        if (opt->long_info) {
            ltdstr_String path = {0};

            ltdstr_Append(&path, filename);
            ltdstr_AppendChar(&path, '/');
            ltdstr_Append(&path, dirent->d_name);

            int r = ListFile_Long_DirEnt(ltdstr_Get(&path), dirent);

            ltdstr_DeInit(&path);

            if (r != 0)
                return r;
        } else {
            printf("%s\n", dirent->d_name);
        }
    }

    return 0;
}

static void
ListFile_Long(char const* name, FileType ft, struct stat* stat) {
    struct passwd* pw = getpwuid(stat->st_uid);
    struct group* gr = getgrgid(stat->st_gid);

    printf("%-10s %-10s %-7s %s\n",
           pw->pw_name == NULL ? "<noname>" : pw->pw_name,  // user
           gr->gr_name == NULL ? "<nogroup>" : gr->gr_name, // group
           kFileTypeName[ft],                               // type
           name);
}

static int
ListFile_Long_DirEnt(char const* path, struct dirent* dirent) {
    struct stat stat;
    if (lstat(path, &stat) != 0) {
        ltlog_Log2(Error, "failed to get file status",
                   "file name:", String(path),
                   "error:", String(strerror(errno)));
        return 1;
    }

    ListFile_Long(dirent->d_name, DirEntType_To_FileType(dirent->d_type),
                  &stat);
    return 0;
}

static void
Usage(void) {
    ltopt_Usage usage = {.name = "lt", .help = "List infomation of files"};

    ltopt_ArgumentHelp arguments[] = {
        {.name = "FILE", .help = "Dir or file to list", .multiple = true},
        {.name = NULL},
    };

    ltopt_OptionHelp options[] = {
        {.name = 'h', .help = "Print this help and exit"},
        {.name = 'l', .help = "Usage a long listing format"},
        {.name = 'd', .help = "Show dot (.) files"},
        {.name = 0},
    };

    usage.arguments = arguments;
    usage.options = options;

    ltopt_WriteUsage(&usage, stderr);
}

static lterr_Code
OnArguments(void* ctx, char name, char const* value) {
    Option* opt = (Option*)ctx;

    switch (name) {
        case 0: lta_Append(opt->files, value); break;
        case 'h': opt->help = true; break;
        case 'l': opt->long_info = true; break;
        case 'd': opt->show_dot = true; break;
        default:
            ltlog_Log1(Error, "invalid option",
                       "option name:", CharQuote(name));
            return lterr_kError;
    }

    return lterr_kOk;
}

int
ls_Run(int argc, char const** argv) {
    Option opt = {0};
    int r;

    lterr_Code code = ltopt_Parse(argc, argv, "", OnArguments, &opt);
    if (code != 0) {
        ltlog_Log1(Error, "failed to parse command line arguments",
                   "error:", String(lterr_Name(code)));
        r = 1;
        goto end;
    }

    if (opt.help) {
        Usage();
        r = 0;
        goto end;
    }

    if (opt.files.len == 0) {
        r = ListPath(".", &opt);
        goto end;
    }

    for (lt_USize i = 0; i < opt.files.len; ++i) {
        int rtmp = ListPath(opt.files.data[i], &opt);
        if (rtmp != 0)
            r = rtmp;
    }

    r = 0;
end:
    lta_DeInit(opt.files);
    return r;
}
